/**
 * MapUtils.java
 * Created at 2016年10月31日
 * Created by Jackie Liu
 * Copyright (C) 2016 Jackie Liu, All rights reserved.
 */
package com.huabo.utils;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * <p>ClassName: MapUtils</p>
 * <p>Description: 地图操作工具类</p>
 * <p>Author: Jackie Liu</p>
 * <p>Date: 2016年10月31日</p>
 */
public class MapUtils {

	private static double DEF_PI = Math.PI; // PI
	private static double DEF_2PI = Math.PI * 2; // 2*PI
	private static double DEF_PI180 = Math.PI / 180; // PI/180.0
	//private static double DEF_R = 6370693.5; // 地球半径
	private static double DEF_R = 6371004; // 地球半径
	
	/**
	 * 是否有 横断<br/>
	 * 参数为四个点的坐标
	 * 
	 * @param px1
	 * @param py1
	 * @param px2
	 * @param py2
	 * @param px3
	 * @param py3
	 * @param px4
	 * @param py4
	 * @return
	 */
	public static boolean isIntersect(double px1, double py1, double px2, double py2, double px3, double py3, double px4, double py4) {
		boolean flag = false;
		double d = (px2 - px1) * (py4 - py3) - (py2 - py1) * (px4 - px3);
		if (d != 0) {
			double r = ((py1 - py3) * (px4 - px3) - (px1 - px3) * (py4 - py3)) / d;
			double s = ((py1 - py3) * (px2 - px1) - (px1 - px3) * (py2 - py1)) / d;
			if ((r >= 0) && (r <= 1) && (s >= 0) && (s <= 1)) {
				flag = true;
			}
		}
		return flag;
	}

	/**
	 * 目标点是否在目标边上边上<br/>
	 * 
	 * @param px0 目标点的经度坐标
	 * @param py0 目标点的纬度坐标
	 * @param px1 目标线的起点(终点)经度坐标
	 * @param py1 目标线的起点(终点)纬度坐标
	 * @param px2 目标线的终点(起点)经度坐标
	 * @param py2 目标线的终点(起点)纬度坐标
	 * @return
	 */
	public static boolean isPointOnLine(double px0, double py0, double px1, double py1, double px2, double py2) {
		boolean flag = false;
		double ESP = 1e-9;// 无限小的正数
		if ((Math.abs(Multiply(px0, py0, px1, py1, px2, py2)) < ESP) && ((px0 - px1) * (px0 - px2) <= 0)
				&& ((py0 - py1) * (py0 - py2) <= 0)) {
			flag = true;
		}
		return flag;
	}

	public static double Multiply(double px0, double py0, double px1, double py1, double px2, double py2) {
		return ((px1 - px0) * (py2 - py0) - (px2 - px0) * (py1 - py0));
	}

	/**
	 * 判断目标点是否在多边形内(由多个点组成)<br/>
	 * 
	 * @param px 目标点的经度坐标
	 * @param py 目标点的纬度坐标
	 * @param polygonXA 多边形的经度坐标集合
	 * @param polygonYA 多边形的纬度坐标集合
	 * @return
	 */
	public static boolean isPointInPolygon(double px, double py, ArrayList<Double> polygonXA, ArrayList<Double> polygonYA) {
		boolean isInside = false;
		double ESP = 1e-9;
		int count = 0;
		double linePoint1x;
		double linePoint1y;
		double linePoint2x = 180;
		double linePoint2y;

		linePoint1x = px;
		linePoint1y = py;
		linePoint2y = py;

		for (int i = 0; i < polygonXA.size() - 1; i++) {
			double cx1 = polygonXA.get(i);
			double cy1 = polygonYA.get(i);
			double cx2 = polygonXA.get(i + 1);
			double cy2 = polygonYA.get(i + 1);
			// 如果目标点在任何一条线上
			if (isPointOnLine(px, py, cx1, cy1, cx2, cy2)) {
				return true;
			}
			// 如果线段的长度无限小(趋于零)那么这两点实际是重合的，不足以构成一条线段
			if (Math.abs(cy2 - cy1) < ESP) {
				continue;
			}
			// 第一个点是否在以目标点为基础衍生的平行纬度线
			if (isPointOnLine(cx1, cy1, linePoint1x, linePoint1y, linePoint2x, linePoint2y)) {
				// 第二个点在第一个的下方,靠近赤道纬度为零(最小纬度)
				if (cy1 > cy2) {
                    count++;
                }
			}
			// 第二个点是否在以目标点为基础衍生的平行纬度线
			else if (isPointOnLine(cx2, cy2, linePoint1x, linePoint1y, linePoint2x, linePoint2y)) {
				// 第二个点在第一个的上方,靠近极点(南极或北极)纬度为90(最大纬度)
				if (cy2 > cy1) {
                    count++;
                }
			}
			// 由两点组成的线段是否和以目标点为基础衍生的平行纬度线相交
			else if (isIntersect(cx1, cy1, cx2, cy2, linePoint1x, linePoint1y, linePoint2x, linePoint2y)) {
				count++;
			}
		}
		if (count % 2 == 1) {
			isInside = true;
		}

		return isInside;
	}
	
	/**
	 * 功能说明：计算两个地图坐标点之间的距离（近距离计算）
	 * @param lng1 起点经度
	 * @param lat1 起点纬度
	 * @param lng2 终点经度
	 * @param lat2 终点纬度
	 * @return 单位米
	 */
	public static double getShortDistance(double lng1, double lat1, double lng2, double lat2) {
		double ew1, ns1, ew2, ns2;
		double dx, dy, dew;
		double distance;
		// 角度转换为弧度
		ew1 = Math.toRadians(lng1);
		ns1 = Math.toRadians(lat1);
		ew2 = Math.toRadians(lng2);
		ns2 = Math.toRadians(lat2);
		
		// 经度差
		dew = ew1 - ew2;
		// 若跨东经和西经180 度，进行调整
		if (dew > DEF_PI){
			dew = DEF_2PI - dew;
		} else if (dew < -DEF_PI){
			dew = DEF_2PI + dew;
		}
			
		dx = DEF_R * Math.cos(ns1) * dew; 	// 东西方向长度(在纬度圈上的投影长度)
		dy = DEF_R * (ns1 - ns2); 			// 南北方向长度(在经度圈上的投影长度)
		// 勾股定理求斜边长
		distance = Math.sqrt(dx * dx + dy * dy);
		return distance;
	}

	/**
	 * 功能说明：计算两个地图坐标点之间的距离（远距离计算）
	 * @param lng1 起点经度
	 * @param lat1 起点纬度
	 * @param lng2 终点经度
	 * @param lat2 终点纬度
	 * @return 单位米
	 */
	public static double getLongDistance(double lng1, double lat1, double lng2, double lat2) {
		double ew1, ns1, ew2, ns2;
		double distance;
		
		// 角度转换为弧度
		ew1 = lng1 * DEF_PI180;
		ns1 = lat1 * DEF_PI180;
		ew2 = lng2 * DEF_PI180;
		ns2 = lat2 * DEF_PI180;
		
		// 求大圆劣弧与球心所夹的角(弧度)
		distance = Math.sin(ns1) * Math.sin(ns2) + Math.cos(ns1) * Math.cos(ns2) * Math.cos(ew1 - ew2);
		// 调整到[-1..1]范围内，避免溢出
		if (distance > 1.0){
			distance = 1.0;
		} else if (distance < -1.0){
			distance = -1.0;
		}
		
		// 求大圆劣弧长度
		distance = DEF_R * Math.acos(distance);
		return distance;
	}
	
	/**
	 * <p>Description: 判断目标点是距离多边形的最短距离</p>
	 * @param point 坐标点[经度lng, 纬度lat]
	 * @param pointsStr 
	 * 规则区域的坐标点字符串，坐标点的格式是：[经度lng, 纬度lat]|[经度lng, 纬度lat]<br/>
	 * 如：117.2222,31.825232|117.221768,31.820231|117.226691,31.820385|117.226601,31.824097
	 * @return
	 */
	public static double getOutShortDis(double[] point, String pointsStr) {
		String[] pointArray = pointsStr.split("\\|");
		List<double[]> pointList = new ArrayList<double[]>();
		ArrayList<Double> polygonXA = new ArrayList<Double>();//多边形的经度坐标集合
		ArrayList<Double> polygonYA = new ArrayList<Double>();//多边形的纬度坐标集合
		
		for (String pointStr : pointArray) {
			String[] array = pointStr.split(",");
			double lng = Double.parseDouble(array[0]);
			double lat = Double.parseDouble(array[1]);
			pointList.add(new double[]{lng, lat});
			polygonXA.add(lng);
			polygonYA.add(lat);
		}
		
		boolean isIn = isPointInPolygon(point[0], point[1], polygonXA, polygonYA);
		if (isIn) {
			return -1;
		}
		return getShortDis(point, pointList);
	}

	/**
	 * <p>Description: 获取坐标点偏移某一区域的最短距离</p>
	 * @param point 坐标点[经度lng, 纬度lat]
	 * @param pointList 不规则区域的坐标点，坐标点的格式是：[经度lng, 纬度lat]
	 * @return
	 */
	public static double getShortDis(double[] point, List<double[]> pointList) {
		// 得到点到范围上每个点的距离。排序拿到最短的2个。以这2个点+外点 做三角形算最短距离
		double[] disArray = new double[pointList.size()];
		for (int i = 0, size = pointList.size(); i < size; i++) {
			double[] shapePoint = pointList.get(i);
			double distance = getLongDistance(point[0], point[1], shapePoint[0], shapePoint[1]);// 计算地图上点到点的距离
			disArray[i] = distance;
		}
		
		//复制数组并升序排序，为的是不影响原数组的数据
		double[] newArray = Arrays.copyOf(disArray, disArray.length);
		Arrays.sort(newArray);

		//以最短的两条边找到合适的三角形
		double minDistance = newArray[0];
		double minDistance2 = newArray[1];
		double[] pointB = {0.0, 0.0}, pointC = {0.0, 0.0};
		int m = 0, n = 0;
		//以距离得到AB的坐标点
		for (int i = 0; i < disArray.length; i++) {
			if (minDistance == disArray[i]) {
				pointB = pointList.get(i);
				m = i;
			}
			if (minDistance2 == disArray[i]) {
				pointC = pointList.get(i);
				n = i;
			}
		}
		//以2点下标差值的绝对值判断仍需要计算几个多边形
		int pointNum = Math.abs(m - n);
		boolean isNeighber = (pointNum == 1);//判断pointB和pointC是否相邻的两个点
		double[] triangleSideLength = getTriangleSideLength(point, pointB, pointC);
		boolean isAcuteAngle = isAcuteAngle(triangleSideLength);//判断point, pointB, pointC组成的三角形是否锐角三角形
		
		//距离最近的两个多边形点是相邻的两个点
		if (isNeighber) {
			if (isAcuteAngle) {
				return getTriangleHeight(triangleSideLength);
			} else {
				return minDistance;
			}
		} else {//距离最近的两个多边形点不是相邻的，需要计算中间所有点
			List<Double> hArray = new ArrayList<Double>();
			// 取到中间的每个点，组合三角形。
			int minIndex = (m > n ? n : m);
			for (int k = minIndex + 1, maxIndex = minIndex + pointNum; k <= maxIndex; k++) {
				double[] _pointB = pointList.get(k - 1);
				double[] _pointC = pointList.get(k);
				double[] _triangleSideLength = getTriangleSideLength(point, _pointB, _pointC);
				boolean _isAcuteAngle = isAcuteAngle(_triangleSideLength);
				if (_isAcuteAngle) {
					hArray.add(getTriangleHeight(_triangleSideLength));
				} else {
					double _minDistance = disArray[k -1];
					double _minDistance2 = disArray[k];
					hArray.add(_minDistance > _minDistance2 ? _minDistance2 : _minDistance);
				}
			}
			if (hArray.size() == 0) {
				return minDistance;
			} else {
				// 取数组中最小的那个值
				double _minDistance = minDistance;
				for (double dis : hArray) {
					if (dis < _minDistance) {
						_minDistance = dis;
					}
				}
				return _minDistance;
			}
		}
		//return -1;
	}
	
	/**
	 * <p>
	 * Description: 判断外点与多边形的另外两个坐标点组成的角度是否为锐角<br/>
	 * 根据勾股定理可得：<br/>
	 * 两条较短边的平方和大于最长边的平方,此三角形就是锐角三角形；<br/>
	 * 两条较短边的平方和小于最长边的平方,此三角形就是钝角三角形；<br/>
	 * 两条边短边的平方和等于最长边的平方,此三角形就是直角三角形
	 * </p>
	 * @param triangleSideLength 三角形边长数组
	 * @return
	 */
	public static boolean isAcuteAngle(double[] triangleSideLength) {
		/*double[] array = Arrays.copyOf(triangleSideLength, triangleSideLength.length);
		Arrays.sort(array);
		double a = array[0];
		double b = array[1];
		double c = array[2];
		double result = a * a + b * b - c * c;
		return result > 0;*/
		
		double ab = triangleSideLength[0];
		double ac = triangleSideLength[1];
		double bc = triangleSideLength[2];
		
		boolean result = (ab * ab + bc * bc - ac * ac) > 0 &&
				(ac * ac + bc * bc - ab * ab) > 0;
		return result;
	}
	
	/**
	 * <p>Description: 计算三个坐标点组成的三角形三个边长</p>
	 * @param pointA 坐标点A [经度lng, 纬度lat]
	 * @param pointB 坐标点B [经度lng, 纬度lat]
	 * @param pointC 坐标点C [经度lng, 纬度lat]
	 * @return
	 */
	public static double[] getTriangleSideLength(double[] pointA, double[] pointB, double[] pointC) {
		double ab, ac, bc;
		ab = getLongDistance(pointA[0], pointA[1], pointB[0], pointB[1]);
		ac = getLongDistance(pointA[0], pointA[1], pointC[0], pointC[1]);
		bc = getLongDistance(pointB[0], pointB[1], pointC[0], pointC[1]);
		double[] sideLength = {ab, ac, bc};
		return sideLength;
	}
	
	/**
	 * <p>Description: 根据海伦公式(面积法)获取三角形的高</p>
	 * @param triangleSideLength 三角形的三边边长
	 * @return
	 */
	public static double getTriangleHeight(double[] triangleSideLength) {
		double h;
		double ab = triangleSideLength[0];
		double ac = triangleSideLength[1];
		double bc = triangleSideLength[2];
		
		double p = (ab + ac + bc) / 2; // 半周长
		double S = Math.sqrt(p * (p - ab) * (p - bc) * (p - ac));// 面积
		h = 2 * S / bc;
		return h < ac ? h : ac;
	}
	
	/**
	 * <p>Description: 计算经纬度点对应正方形4个中间点的经度或纬度</p>
	 * @param longitude 经度
	 * @param latitude 纬度
	 * @param distance 附近距离(单位：米)
	 * @return {small纬度, big纬度, small经度, big经度}
	 */
	public static double[] squarePoint(double longitude, double latitude, double distance) {
        // 计算经度弧度,从弧度转换为角度
        double dLongitude = 2 * (Math.asin(Math.sin(distance / (2 * DEF_R)) / Math.cos(Math.toRadians(latitude))));
        dLongitude = Math.toDegrees(dLongitude);
        // 计算纬度角度
        double dLatitude = distance / DEF_R;
        dLatitude = Math.toDegrees(dLatitude);
        // 正方形中心点的经度或纬度
        double biglg = longitude + dLongitude;//大经度
        double smallLg = longitude - dLongitude;//小经度
        double biglt = latitude + dLatitude;//大纬度
        double smallLt = latitude - dLatitude;//小纬度
        
        double[] square = {smallLt, biglt, smallLg, biglg};
        return square;
    }
	
	//根据距离，得到下标
	/*private static int getIndex(double distance, double[] disArray) {
		for (int i = 0; i < disArray.length; i++) {
			if (distance == disArray[i]) {
				return i;
			}
		}
		return -1;
	}*/
	
	//是否相邻：
	/*private static boolean isNeighber(double[] triangleSideLength, double[] disArray) {
		double ab = triangleSideLength[0];
		double ac = triangleSideLength[1];
		//double bc = triangleSideLength[2];

		int m, n;
		// 找出下标：
		m = getIndex(ab, disArray);
		n = getIndex(ac, disArray);

		return Math.abs(m - n) == 1;
	}*/
	
	public static void main(String[] args) {
		/*double mLat1 = 30.67124; 	// point1纬度
		double mLng1 = 117.49797; 	// point1经度
		double mLat2 = 30.64712;	// point2纬度
		double mLng2 = 117.39612;	// point2经度
		
		double distanceByShort = getShortDistance(mLng1, mLat1, mLng2, mLat2);
		System.out.println(distanceByShort);
		
		double distanceByLong = getLongDistance(mLng1, mLat1, mLng2, mLat2);
		System.out.println(distanceByLong);
		
		System.out.println("@@@@@@@@@ = " + getTriangleHeight(new double[]{3, 5, 4}));*/
		
		String pointsStr = "117.2222,31.825232|117.221768,31.820231|117.226691,31.820385|117.226601,31.824097|117.226368,31.824481|117.226008,31.824573";
		
		String[] pointArray = pointsStr.split("\\|");
		List<double[]> list = new ArrayList<double[]>();
		for (String point : pointArray) {
			String[] array = point.split(",");
			double[] d = new double[]{Double.parseDouble(array[0]), Double.parseDouble(array[1])};
			list.add(d);
			//System.out.println("d = " + d[0] + ", " + d[1]);
		}
   		
   		double dis = getShortDis(new double[]{117.227433,31.822067}, list);
   		dis = getOutShortDis(new double[]{117.229803,31.824585}, pointsStr);
   		System.out.println("dis = " + (dis));
   		
   		
   		pointsStr = "117.110517,31.839608|117.475013,31.911728|117.369804,31.66963";
   		pointArray = pointsStr.split("\\|");
		ArrayList<Double> polygonXA = new ArrayList<Double>();//多边形的经度坐标集合
		ArrayList<Double> polygonYA = new ArrayList<Double>();//多边形的纬度坐标集合
		
		for (String pointStr : pointArray) {
			String[] array = pointStr.split(",");
			double lng = Double.parseDouble(array[0]);
			double lat = Double.parseDouble(array[1]);
			polygonXA.add(lng);
			polygonYA.add(lat);
		}
		
		boolean isIn = isPointInPolygon(117.39612, 30.64712, polygonXA, polygonYA);
		System.out.println("isIn = " + isIn);
	}
}
